/*  This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License,or (at
    your option) any later version.
    For more details, see the GNU General Public License (www.fsf.org or
    the COPYING file somewhere in the package)
 */

/*! @file src/packager/Packager.cc
    @brief Bundle packager.
    @author @ref Guillaume_Terrissol
    @date 12th December 2009 - 1st April 2014
    @note This file is distributed under the GPL license.
    Refer to the file COPYING (or http://www.fsf.org) for more information.
 */

#include <cassert>
#include <fstream>
#include <map>
#include <sstream>
#include <stdexcept>

#if   defined(WIN32)
#   include <windows.h>
#else
#   if !defined(ZIP_STD)
#       define ZIP_STD
#   endif   // ZIP_STD
#endif  // defined(WIN32)

#include "Bundle.hh"
#include "Platform.hh"
#include "tinyxml.h"
#include "Utils.hh"
#include "zip.h"

namespace OSGi
{
    /*! @page OSGi_Overview_Page OSGi++ (light) overview
        For long, I kept away from plugins, because of a bad first experience. I changed my mind after using 
        <A HREF="http://pocoproject.org/">POCO</A> <A HREF="http://www.appinf.com/features.html#osp">OSP</A>, a C++
        implementation of  <A HREF="http://www.osgi.org">OSGi</A>, in a project which relied heavily on plugins. @n
        It persuaded me to refactorize a long time project of mine, a <A HREF="http://www.oldschoolgame.com">video
        game</A> and its dedicated editor, which had become both overly complex.@n
        Because <A HREF="http://www.appinf.com/features.html#osp">POCO OSP</A> was not a free framework, and was way
        too big for my humble needs, I decided to write my own, light, C++11 implementation of OSGi. And, my project
        'working name' being &ldquo;<A HREF="http://www.oldschoolgame.com">Old School Game</A>&rdquo;, I found the OSGi
        acronym quite fitting : <b>O</b>ld <b>S</b>chool <b>G</b>ame <b>i</b>ntegration.@n
        Unlike <A HREF="http://www.appinf.com/features.html#osp">OSP</A>, this library provide very few @ref 
        OSGi_Default_Services_Page "services"; it is just a base, to be extended at will.@n
        And unlike <A HREF="http://www.appinf.com/features.html#osp">OSP</A>, it requires no dependency  : a few
        external pieces of code have been added, along with their specific license, to make this library really
        stand-alone (the unit tests still require <A HREF="http://freedesktop.org/wiki/Software/cppunit/">cppunit</A>).
     */
    /*! @page OSGi_Bundlespec_Page Bundle specification
        To be generated, everybundle requires a @b .bundlespec file. It's an @b XML file, describing the bundle
        contents and dependencies.@n
        It is written as follow :
        @htmlonly
<div class="line">&lt;?<span class="stringliteral">xml</span> <span class="keyword">version</span>='1.0'<span class="stringliteral">?&gt;</span>                                            | Standard XML file header.</div>
<div class="line"><span class="stringliteral">&lt;bundlespec</span> <span class="keyword">version</span>="1.0"<span class="stringliteral">&gt;</span>                                       | Specific root element.</div>
<div class="line">    <span class="stringliteral">&lt;manifest&gt;</span>                                                   | The manifest describes the bundle.</div>
<div class="line">        <span class="stringliteral">&lt;name&gt;</span><span class="preprocessor">An example bundle</span><span class="stringliteral">&lt;/name&gt;</span>                           | A description of what the bundle is, or does</div>
<div class="line">        <span class="stringliteral">&lt;symbolicName&gt;</span><span class="preprocessor">fr.osgi.test.example</span><span class="stringliteral">&lt;/symbolicName&gt;</span>        | This is the name of the bundle file, usually the form of a URL address, with reverted components.</div>
<div class="line">        <span class="stringliteral">&lt;version&gt;</span><span class="preprocessor">1.0.0</span><span class="stringliteral">&lt;/version&gt;</span>                                 | The version numbers always holds 3 digits (usually : major, minor, bugfix).</div>
<div class="line">        <span class="stringliteral">&lt;vendor&gt;</span><span class="preprocessor">libOSGi++</span><span class="stringliteral">&lt;/vendor&gt;</span>                               | The bundle provider.</div>
<div class="line">        <span class="stringliteral">&lt;copyright&gt;</span><span class="preprocessor">GNU LGPL v2.1 2013, OldSchoolGame</span><span class="stringliteral">&lt;/copyright&gt;</span> | The copyright, or license.</div>
<div class="line">        <span class="stringliteral">&lt;activator&gt;</span>                                              | The activator is the function to call, after loading the bundle.</div>
<div class="line">            <span class="stringliteral">&lt;class&gt;</span></span><span class="preprocessor">EXAMPLE::Activator</span><span class="stringliteral">&lt;/class&gt;</span>                    | This is the activator function symbol.</div>
<div class="line">        <span class="stringliteral">&lt;/activator&gt;</span>                                             |</div>
<div class="line">    <span class="stringliteral">&lt;/manifest&gt;</span>                                                  |</div>
<div class="line">    <span class="stringliteral">&lt;code&gt;</span>                                                       | This is the list of libraries provided by the bundle. </div>
<div class="line">       <span>fr.osgi.test.example.so,</span>                                  | A single bundle may provide the same library for different platforms.</div>
<div class="line">       <span>fr.osgi.test.example.dll</span>                                  |</div>
<div class="line">    <span class="stringliteral">&lt;/code&gt;</span>                                                      |</div>
<div class="line">    <span class="stringliteral">&lt;files&gt;</span>                                                      |</div>
<div class="line">        <span class="comment">&lt;!-- Any other required file--&gt;</span>                          | Data files, configuration files, etc.</div>
<div class="line">    <span class="stringliteral">&lt;/files&gt;</span>                                                     |</div>
<div class="line"><span class="stringliteral">&lt;/bundlespec&gt;</span>                                                    |</div>
        @endhtmlonly
        @sa Tests bundles
     */


//------------------------------------------------------------------------------
//                                Helper Classes
//------------------------------------------------------------------------------

    /*! @brief Manifest data.
        @version 1.0
        @internal
     */
    struct Manifest
    {
        std::string                                         mName;              //!< Bundle [full] name.
        std::string                                         mSymbolicName;      //!< Bundle filename.
        std::string                                         mVersion;           //!< Bundle version.
        std::string                                         mVendor;            //!< Bundle provider.
        std::string                                         mCopyright;         //!< Bundle copyright.
        std::string                                         mActivatorClass;    //!< Bundle activator.
        std::string                                         mActivatorLibrary;  //!< Activator library.
        std::vector<std::pair<std::string, std::string>>    mDependencies;      //!< Bundle dependencies.
    };


    /*! @brief Bundle specification data.
        @version 1.0
        @internal
     */
    struct BundleSpec
    {
        Manifest                                            mManifest;          //!< Manifest.
        std::vector<std::pair<std::string, std::string>>    mCodeFiles;         //!< Binary files (executables et libraries).
        std::vector<std::pair<std::string, std::string>>    mOtherFiles;        //!< Other files.
    };


    /*! @brief @b .bundlespec file reader
        @version 0.5
        @internal
     */
    class BundleSpecReader
    {
    public:

                            BundleSpecReader(const std::string& pFile); //!< Constructor.
        const BundleSpec&   getSpec() const;                            //!< Specification access.

    private:

        void                readManifest(XmlElement* pElement);         //!< Manifest reader .
        void                readCode(XmlElement* pElement);             //!< Binary file list reader.
        void                readFiles(XmlElement* pElement);            //!< Other file list reader.

        BundleSpec  mSpec;                                              //!< Bundle specification.
    };


//------------------------------------------------------------------------------
//                         Bundle Spec Read : Functions
//------------------------------------------------------------------------------

    /*! @param pFile @b .bndl filename
     */
    BundleSpecReader::BundleSpecReader(const std::string& pFile)
        : mSpec()
    {
        if (!std::fstream{pFile.c_str(), std::fstream::in}.is_open())
        {
            throw std::runtime_error{pFile + " couldn't be opened"};
        }

        TiXmlDocument lBundleSpecFile(pFile.c_str());
        lBundleSpecFile.LoadFile();

        XmlElement* lRoot = lBundleSpecFile.RootElement();

        for(auto* lElt = lRoot->FirstChildElement(); lElt != nullptr; lElt = lElt->NextSiblingElement())
        {
            if      (lElt->ValueStr() == "manifest")
            {
                try
                {
                    readManifest(lElt);
                }
                catch(std::exception& pE)
                {
                    throw std::runtime_error{std::string(pE.what()) + " in file " + pFile};
                }
            }
            else if (lElt->ValueStr() == "code")
            {
                readCode(lElt);
            }
            else if (lElt->ValueStr() == "files")
            {
                readFiles(lElt);
            }
        }
    }


    /*! @return The specification read so far
     */
    const BundleSpec& BundleSpecReader::getSpec() const
    {
        return mSpec;
    }


    /*! Parses the manifest, from its root element.
        @param pElement XML manifest root element
     */
    void BundleSpecReader::readManifest(XmlElement* pElement)
    {
        assert(pElement->ValueStr() == "manifest");

        for(const auto* lElt = pElement->FirstChildElement(); lElt != nullptr; lElt = lElt->NextSiblingElement())
        {
            if (const char* lElementText = lElt->GetText())
            {
                if      (lElt->ValueStr() == "name")
                {
                    mSpec.mManifest.mName = lElementText;
                }
                else if (lElt->ValueStr() == "symbolicName")
                {
                    mSpec.mManifest.mSymbolicName = lElementText;
                }
                else if (lElt->ValueStr() == "version")
                {
                    mSpec.mManifest.mVersion = lElementText;
                }
                else if (lElt->ValueStr() == "vendor")
                {
                    mSpec.mManifest.mVendor = lElementText;
                }
                else if (lElt->ValueStr() == "copyright")
                {
                    mSpec.mManifest.mCopyright = lElementText;
                }
            }
            if (lElt->ValueStr() == "activator")
            {
                for(const auto* lActElt = lElt->FirstChildElement(); lActElt != nullptr; lActElt = lActElt->NextSiblingElement())
                {
                    if (const char* lActEltText = lActElt->GetText())
                    {
                        if      (lActElt->ValueStr() == "class")
                        {
                            mSpec.mManifest.mActivatorClass = lActEltText;
                        }
                        else if (lActElt->ValueStr() == "library")
                        {
                            mSpec.mManifest.mActivatorLibrary = lActEltText;
                        }
                    }
                }
            }
            else if (lElt->ValueStr() == "dependency")
            {
                std::string lSymbolicName;
                std::string lVersion;
                for(const auto* lDepElt = lElt->FirstChildElement(); lDepElt != nullptr; lDepElt = lDepElt->NextSiblingElement())
                {
                    if (const char* lDepEltText = lDepElt->GetText())
                    {
                        if      (lDepElt->ValueStr() == "symbolicName")
                        {
                            lSymbolicName = lDepEltText;
                        }
                        else if (lDepElt->ValueStr() == "version")
                        {
                            lVersion = lDepEltText;
                        }
                    }
                }
                if (!lSymbolicName.empty())
                {
                    mSpec.mManifest.mDependencies.push_back(std::make_pair(lSymbolicName, lVersion));
                }
            }
        }

        // Checks whether all mandatory fields have correctly been filled.
        if (mSpec.mManifest.mSymbolicName.empty())
        {
            throw std::runtime_error{"Manifest : missing symbolic name"};
        }
        if (mSpec.mManifest.mVersion.empty())
        {
            throw std::runtime_error{"Manifest : missing version"};
        }
        if (mSpec.mManifest.mActivatorClass.empty())
        {
            throw std::runtime_error{"Manifest : missing activator"};
        }
    }


    /*! Parses the binary file list.
        @param pElement XML @b code node from the manifest
     */
    void BundleSpecReader::readCode(XmlElement* pElement)
    {
        assert(pElement->ValueStr() == "code");

        std::string lPlatform = Platform::osName() + "/" + Platform::osArchitecture();
        if (const char* lAttribute = pElement->Attribute("platform"))
        {
            lPlatform = lAttribute;
        }
        lPlatform = std::string("bin/") + lPlatform + "/";

        for(const auto* lElt = pElement->FirstChild(); lElt != nullptr; lElt = lElt->NextSibling())
        {
            if (lElt->Type() == XmlElement::TEXT)
            {
                std::string                 lElementText = lElt->ValueStr();
                std::vector<std::string>    lFiles       = splitString(lElementText, ',');
                for(auto& lFile : lFiles)
                {
                    trimString(lFile);
#if defined(DEBUG)
                    auto    lExtension = lFile.rfind('.');
                    if (lExtension == std::string::npos)
                    {
                        throw std::runtime_error{lFile + " is not a valid file"};
                    }
                    lFile.insert(lExtension, POSTFIX);
#endif  // defined(DEBUG)
                    std::string lFileInArchive = lPlatform + lFile.substr(lFile.rfind('/') + 1);
                    mSpec.mCodeFiles.push_back(std::make_pair(lFile, lFileInArchive));
                }
            }
        }
    }


    /*! Parses the other file list.
        @param pElement XML @b files node from the manifest
     */
    void BundleSpecReader::readFiles(XmlElement* pElement)
    {
        assert(pElement->ValueStr() == "files");

        for(const auto* lElt = pElement->FirstChild(); lElt != nullptr; lElt = lElt->NextSibling())
        {
            if (lElt->Type() == XmlElement::TEXT)
            {
                std::string lElementText = lElt->ValueStr();
                std::vector<std::string>    lFiles = splitString(lElementText, ',');
                for(auto& lFile : lFiles)
                {
                    trimString(lFile);
                    mSpec.mOtherFiles.push_back(std::make_pair(lFile, lFile));
                }
            }
        }
    }


//------------------------------------------------------------------------------
//                                Helper Function
//------------------------------------------------------------------------------

    /*! Creates a manifest file contents (META-INF/manifest.mf)
        @param pManifest Manifest data
        @return @p pManifest contents as plain text (in norder to fill a manifest.mf
        file)
        @internal
     */
    std::string WriteManifest(const Manifest& pManifest)
    {
        std::ostringstream  lData;

        lData << "Manifest-Version: 1.0\n";
        lData << "Bundle-Name: " << pManifest.mName << '\n';
        lData << "Bundle-SymbolicName: " << pManifest.mSymbolicName << '\n';
        lData << "Bundle-Version: " << pManifest.mVersion << '\n';
        lData << "Bundle-Vendor: " << pManifest.mVendor << '\n';
        lData << "Bundle-Copyright: " << pManifest.mCopyright << '\n';
        lData << "Bundle-Activator: " << pManifest.mActivatorClass;
        if (!pManifest.mActivatorLibrary.empty())
        {
            lData << "; library = " << pManifest.mActivatorLibrary;
        }
        lData << '\n';
        if (!pManifest.mDependencies.empty())
        {
            for(auto lBndl = begin(pManifest.mDependencies); lBndl != end(pManifest.mDependencies); ++lBndl)
            {
                lData << (lBndl == begin(pManifest.mDependencies) ? "Require-Bundle: " : "                ");
                lData <<  lBndl->first;
                if (!lBndl->second.empty())
                {
                    lData << "; bundle-version = " << lBndl->second;
                }
                if (std::distance(lBndl, end(pManifest.mDependencies)) != 1)
                {
                    lData << ',';
                }
                lData << '\n';
            }
        }

        return lData.str();
    }
}


const std::string    kSpecExt = ".bundlespec";  //!< Bundle specification file extension.

//------------------------------------------------------------------------------
//                     Helper Functions for Argument Parsing
//------------------------------------------------------------------------------

/*! Checks whether a program argument starts with a given token.
    @param pArgument String to check
    @param pToken    Token to search for
    @retval true  if @p pArgument starts with @p pToken
    @retval false otherwise
 */
bool startsWith(const std::string& pArgument, const char* pToken)
{
    return (pArgument.compare(0, strlen(pToken), pToken) == 0);
}

/*! @param pArgument A program argument, the useful value of which to retrieve
    @param pToken    @p pArgument first characters
    @return The @p pArgument value after @p pToken
    @note No check is done on @p pArgument starting with @p pToken
 */
std::string extractAfter(const std::string& pArgument, const char* pToken)
{
    return pArgument.substr(strlen(pToken));
}

//------------------------------------------------------------------------------
//                                     Main
//------------------------------------------------------------------------------

/*! Fonction main()
    @param pArgC Argument count
    @param pArgV Argument list
    @note Usage:@n
    packager [options] file.bundlespec
    Options are :
    -I\<path\>
    -o \<output file\>
    -f\<file\>=\<file in bundle\>
    -h|--help
    -v|--version
 */
int main(int pArgC, char* pArgV[])
{
    try
    {
        if (pArgC < 2)
        {
            throw std::invalid_argument{"Missing arguments"};
        }

        std::vector<std::string>            lInputDirs;
        std::map<std::string, std::string>  lRenamedFiles;
        std::string                         lInputFile  = "";
        std::string                         lOutputFile = "";

        // Parses arguments.
        for(int lArgC = 1; lArgC < pArgC; ++lArgC)
        {
            std::string lArg{pArgV[lArgC]};
            if      (startsWith(lArg, "-I"))
            {
                if (2 < lArg.length())  // Is there really a directory ? (paranoid mode on)
                {
                    lInputDirs.push_back(extractAfter(lArg, "-I"));

                    if (*lInputDirs.back().rbegin() != '/')
                    {
                        lInputDirs.back().append(1, '/');  // Add trailing slash, just in case.
                    }
                }
            }
            else if (startsWith(lArg, "-o"))
            {
                if (lOutputFile.empty() && ((lArgC + 1) < pArgC))
                {
                    lOutputFile = pArgV[++lArgC];
                }
                else
                {
                    throw std::invalid_argument{"Output file defined mode than once"};
                }
            }
            else if (startsWith(lArg, "-f"))
            {
                auto    lFilenames = OSGi::splitString(extractAfter(lArg, "-f"), '=');
                if (lFilenames.size() == 2)
                {
                    lRenamedFiles[lFilenames[0]] = lFilenames[1];
                }
                else
                {
                    throw std::invalid_argument{std::string("Invalid alias : ") + lArg};
                }
            }
            else if ((lArg == "-h") || (lArg == "--help"))
            {
                std::cout << "Usage:\n" +
                             std::string(pArgV[0]) + " [options] file.bundlespec\n"
                             "Options are :\n"
                             "-I<include path> {n}        adds a path to the input folders\n"
                             "-o <output file>            sets the output bundle name\n"
                             "-f<file>=<file in bundle>   creates an alias for a file in the bundle\n"
                             "-h|--help                   displays this help and quits\n"
                             "-v|--version                displays the version and quits\n";
                // No need to go further.
                return 0;
            }
            else if ((lArg == "-v") || (lArg == "--version"))
            {
                std::cout << "OSGi Packager " OSGI_VERSION "\n"
                             "This program is free software; you can redistribute it and/or modify\n"
                             "it under the terms of the GNU General Public License as published by\n"
                             "the Free Software Foundation; either version 2 of the License,or (at\n"
                             "your option) any later version.\n";
                // No need to go further.
                return 0;
            }
            else if (lInputFile.empty())
            {
                lInputFile = lArg;
            }
            else
            {
                throw std::invalid_argument{"Input file defined mode than once"};
            }
        }
        lInputDirs.push_back(""); // Current directory is always considered last.

        // Checks arguments values, and set defaults, if needed.
        size_t  lExtPos = lInputFile.rfind(kSpecExt);
        if ((lExtPos == std::string::npos) || (lExtPos + kSpecExt.length() != lInputFile.length()))
        {
            throw std::invalid_argument{"Expected bundlespec file"};
        }

        OSGi::BundleSpecReader  lReader(lInputFile);
        const OSGi::BundleSpec& lSpec = lReader.getSpec();
        std::string             lBundleName = lSpec.mManifest.mSymbolicName;
        if (lOutputFile.empty())
        {
            lOutputFile = lBundleName + "_" + lSpec.mManifest.mVersion + OSGi::kBndlExt;
        }

        HZIP    lZip = CreateZip(lOutputFile.c_str(), "");
        // Creates a new archive.
        if (lZip != nullptr)
        {
            std::string  lManifest = WriteManifest(lSpec.mManifest);
            ZipAdd(lZip, "META-INF/manifest.mf", const_cast<char*>(lManifest.c_str()), lManifest.length());

            auto    lAddZip = [&](const std::vector<std::pair<std::string, std::string>>& pFiles) -> void
            {
                for(const auto& lPair : pFiles)
                {
                    for(const auto& lInputDir : lInputDirs)
                    {
                        auto        lAlias      = lRenamedFiles.find(lPair.first);
                        std::string lInputFile  = lInputDir + ((lAlias != end(lRenamedFiles)) ? lAlias->second : lPair.first);

                        if (std::fstream{lInputFile.c_str(), std::fstream::in}.is_open())
                        {
                            ZipAdd(lZip, lPair.second.c_str(), lInputFile.c_str());
                            break;
                        }
                    }
                }
            };
            lAddZip({ { (lBundleName + ".properties"), (lBundleName + ".properties") } });
            lAddZip(lSpec.mCodeFiles);
            lAddZip(lSpec.mOtherFiles);

            CloseZip(lZip);
        }
    }
    catch(std::exception& pE)
    {
        std::cerr << "STL exception " << typeid(pE).name() << " : " << pE.what() << std::endl;
        return 1;
    }
    catch(...)
    {
        std::cerr << "Unknown exception : aborting..." << std::endl;
        return 2;
    }

    return 0;
}
